package com.webssh.service.handler.terminal;

import com.jcraft.jsch.*;
import com.webssh.service.handler.WSHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 *  
 * @author zhanshuw
 *     	   2018年6月23日
 * 
 */
public class TerminalClient {

	private static Logger logger = LoggerFactory.getLogger(TerminalClient.class);
    private String httpSessionId;//打开该终端的SessionId
    private WebSocketSession webSocketSession;
    private ChannelShell channelShell;
    private Session session;
    private Terminal terminal;
    private BufferedWriter writer;
    private String uploadFileToken;

    //控制命令(exit)终端退出相关变量
    private volatile boolean sendEnter;
    private volatile boolean receiveEnd = false;
    private volatile StringBuffer sendBuffer = new StringBuffer();

    //连接时长相关变量
    public static final int SERVER_ALIVE_INTERVAL = 60 * 1000;
    public static final int SESSION_TIMEOUT = 60000;
    public static final int CHANNEL_TIMEOUT = 60000;

    private boolean closed = false;
    
    private String sendTempCmdId;//
	private volatile String pwdPath;//pwd路径
	private volatile CountDownLatch cdl ;
	private Thread run;

    public TerminalClient(Terminal terminal) {
    	this.terminal = terminal;
    }

    public void init(WebSocketSession ws) throws IOException, JSchException {
    	if(ws == null || !ws.isOpen()){
    		this.disconnect();
    		return;
    	}
    	this.webSocketSession = ws;
    	JSch jSch = new JSch();
    	this.session = jSch.getSession(terminal.getUserName(), terminal.getHost(), terminal.getPort());
    	this.session.setPassword(terminal.getPassword());
    	this.session.setConfig("StrictHostKeyChecking", "no");
    	this.session.setConfig("PreferredAuthentications", "publickey,keyboard-interactive,password");
    	this.session.setServerAliveInterval(SERVER_ALIVE_INTERVAL);
    	this.session.connect(SESSION_TIMEOUT);
    	TerminalSession.put(webSocketSession, this);
    	this.httpSessionId = (String) webSocketSession.getAttributes().get(HttpSessionHandshakeInterceptor.HTTP_SESSION_ID_ATTR_NAME);
    	this.sendTempCmdId = UUID.randomUUID() + httpSessionId;
    	try {
    		int cols = Integer.parseInt(webSocketSession.getAttributes().get("cols").toString());
    		int rows = Integer.parseInt(webSocketSession.getAttributes().get("rows").toString());
    		int width = Integer.parseInt(webSocketSession.getAttributes().get("width").toString());
    		int height = Integer.parseInt(webSocketSession.getAttributes().get("height").toString());
    		this.openTerminal(cols,rows,width,height);
    	} catch (Exception e) {
    		if (e.getLocalizedMessage().replaceAll("\\s+", "").contentEquals("Operationtimedout")) {
    			webSocketSession.sendMessage(new TextMessage("Sorry! Connect timed out, please try again.\r\n"));
    		} else {
    			webSocketSession.sendMessage(new TextMessage("Sorry! Operation error(" + e.getMessage() +"), please try again.\r\n"));
    		}
    		this.disconnect();
    	}
    }
    public void init(String httpSessionId) throws IOException, JSchException {
    	this.webSocketSession = WSHandler.getWebSocketSession(httpSessionId);
    	init(webSocketSession);
	}
    private void openTerminal(final int cols, int rows, int width, int height) throws Exception {
        this.channelShell = (ChannelShell) session.openChannel("shell");
        this.channelShell.setPtyType("xterm", cols, rows, width, height);
        final InputStream inputStream = this.channelShell.getInputStream();
        this.writer = new BufferedWriter(new OutputStreamWriter(this.channelShell.getOutputStream(), "UTF-8"));
        this.channelShell.connect();
        this.run = new Thread(new Runnable() {
            @Override
            public void run() {
                byte[] buffer = new byte[1024 * 4];
                StringBuilder builder = new StringBuilder();
                try {
                    while (webSocketSession != null && webSocketSession.isOpen() && !Thread.interrupted()) {
                        builder.setLength(0);
                        int size = inputStream.read(buffer);
                        if (size == -1) {
                            break;
                        }
                        for (int i = 0; i < size; i++) {
                            char chr = (char) (buffer[i] & 0xff);
                            builder.append(chr);
                        }
                        //取到linux远程机器输出的信息发送给前端
                        String message = builder.toString();
                        message = new String(message.getBytes(getEncoding(message)), "UTF-8");
                        if (cdl != null && pwdPath == null) {
                            if (message.startsWith(sendTempCmdId)) {
                            	if (pwdPath != null || message.contains("echo")) {
                            		continue;
                            	}
                            	pwdPath = message.replace(sendTempCmdId, "").replaceAll("\\r\\n.*", "") + "/";
                            	logger.debug("current pwd result:{}", pwdPath);
                            	cdl.countDown();
                            	cdl = null;
                            }
                        }else{
                        	logger.debug("out:{}", message);
                        	webSocketSession.sendMessage(new TextMessage(message));
                        	if (sendEnter) {
                        		String trimMessage = message.replaceAll("\r\n", "");
                        		String bash = sendBuffer.toString();
                        		if (trimMessage == null || trimMessage.length() == 0) {
                        			receiveEnd = false;
                        		} else {
                        			if (bash.equals("exit") && trimMessage.equals("logout")) {
                        				disconnect();
                        			}
                        			receiveEnd = true;
                        		}
                        		if (receiveEnd) {
                        			if(bash.length() > 0){
                        				recordBash(bash);
                        			}
                        			sendBuffer.setLength(0);
                        		}
                        	}
                        }
                    }
                    if(webSocketSession != null && webSocketSession.isOpen()){
                    	webSocketSession.sendMessage(new TextMessage("jSch Terminal closed.\r\n"));
                    }
                    disconnect();
                } catch (Exception e) {
                	logger.error(e.getMessage(), e);
                	try {
						webSocketSession.sendMessage(new TextMessage("Sorry! exception occurred: " + e.getMessage() + ". jSch Terminal was closed.\r\n"));
						TerminalClient.this.disconnect();
					} catch (IOException e1) {
					}
                }
            }
        });
        run.start();
    }

    protected void recordBash(String bash) {
		logger.info("客户端输入命令：{}", bash);
	}

	/**
     * 向ssh终端输入内容
     *
     * @param message
     * @throws IOException
     */
    public void write(String message) throws IOException {
        if (message.equals("\r")) {
            if (closed) {
                disconnect();
            } else {
                sendEnter = true;
            }
        } else {
            sendEnter = false;
            sendBuffer.append(message);
        }

        if (writer != null) {
            writer.write(message);
            writer.flush();
        }
    }

    /**
     * 上传文件
     * @author  zhanshuw
     * 			2018年6月23日
     * @param in
     * @param dst
     * @param fileSize
     * @throws Exception
     */
    public void upload(InputStream in, String dst, long fileSize) throws Exception {
    	ChannelSftp channelSftp = null;
    	TerminalSftpMonitor m = null;
        try {
			if (dst.startsWith("./")) {
				String pwd = getPwdPath();
			    dst = dst.replaceFirst("\\./", pwd);
			} else if (dst.startsWith("~")) {
			    dst = dst.replaceAll("~/|~", "");
			}
			logger.debug("Sftp upload file target path:{}", dst);
			channelSftp = (ChannelSftp) this.session.openChannel("sftp");
			channelSftp.connect(CHANNEL_TIMEOUT);
			m = new TerminalSftpMonitor(fileSize);
			channelSftp.put(in, dst, m);
		}finally{
			if (channelSftp != null) {
    			channelSftp.exit();
    			channelSftp.disconnect();
    		}
			if(m != null){
				m.close();
			}
		}
    }
    /**
     * 下载文件
     * @author  zhanshuw
     * 			2018年6月23日
     * @param out
     * @param src
     * @param fileSize
     * @throws Exception
     */
    public void get(WriteData w, String src) throws Exception {
    	ChannelSftp channelSftp = null;
    	TerminalSftpMonitor m = null;
    	try {
    		channelSftp = (ChannelSftp) this.session.openChannel("sftp");
    		channelSftp.connect(CHANNEL_TIMEOUT);
    		m = new TerminalSftpMonitor();
    		if (src.startsWith("./")) {
				channelSftp.cd(getPwdPath());
			}
    		InputStream in = channelSftp.get(src, m);
    		w.writeDate(in, m.getFileSize());
    	}finally{
			if (channelSftp != null) {
    			channelSftp.exit();
    			channelSftp.disconnect();
    		}
			if(m != null){
				m.close();
			}
    	}
    }
    
    private synchronized String getPwdPath() throws IOException, InterruptedException{
    	String res = null;
		try {
			cdl = new CountDownLatch(1);
			if(pwdPath != null)pwdPath = null;
			write(String.format("echo %s$(pwd)\r", this.sendTempCmdId));
			cdl.await(5, TimeUnit.SECONDS);
			if(pwdPath == null){
				throw new RuntimeException("Sftp upload file target path error!");
			}
			res = pwdPath.toString();
		} finally {
			pwdPath = null;
		}
    	return res;
    }
    public interface WriteData{
    	void writeDate(InputStream in, long fileSize) throws IOException;
    }

    public void disconnect() throws IOException {
    	closed = true;
    	this.run.interrupt();
        if (writer != null) {
            writer.close();
            writer = null;
        }
        if (session != null) {
            session.disconnect();
            session = null;
        }
        if(webSocketSession != null){
        	TerminalSession.remove(webSocketSession);
        	if(webSocketSession.isOpen()){
        		webSocketSession.close();
        	}
        }
    }

    public void resize(Integer cols, Integer rows, Integer width, Integer height) throws IOException {
        channelShell.setPtySize(cols, rows, width, height);
    }

    public Terminal getTerminal() {
        return terminal;
    }

    public void setTerminal(Terminal terminal) {
        this.terminal = terminal;
    }

    public boolean isClosed() {
    	if(!this.channelShell.isConnected() || !this.channelShell.isConnected()){
    		try {
				disconnect();
			} catch (IOException e) {
				e.printStackTrace();
			}
    	}
        return closed;
    }

    public WebSocketSession getWebSocketSession() {
        return webSocketSession;
    }

    public String getHttpSessionId() {
        return httpSessionId;
    }

    public String getUploadFileToken() {
		return uploadFileToken;
	}

	public void setUploadFileToken(String uploadFileToken) {
		this.uploadFileToken = uploadFileToken;
	}

	public static String getEncoding(String text) {
        String encode = "ISO-8859-1";
        try {
            if (text.equals(new String(text.getBytes(encode), encode))) {
                return encode;
            }
        } catch (Exception exception1) {
        }

        encode = "GB2312";
        try {
            if (text.equals(new String(text.getBytes(encode), encode))) {
                return encode;
            }
        } catch (Exception exception) {}

        encode = "UTF-8";
        try {
            if (text.equals(new String(text.getBytes(encode), encode))) {
                return encode;
            }
        } catch (Exception exception2) {
        }

        encode = "GBK";
        try {
            if (text.equals(new String(text.getBytes(encode), encode))) {
                return encode;
            }
        } catch (Exception exception3) {
        }
        return encode;
    }
}
